import os
import platform
import shutil
import itertools

from made.util import command, misc
from made.contrib import compilersettings
from made import runtime

_user_config_entries = {}
def _user_config_function(function):
    _user_config_entries[function.__name__] = function
    return function

# This function doesn't create a task, so it is not part of user_config.
def read_file_now(file_name):
    madefile = runtime.current_madefile()
    file_path = madefile.full_path_to_file(file_name)
    madefile.require_reload_on_file_change(file_name)
    return _open_and_read(file_path)

def _open_and_read(file_path):
    with open(file_path, "rb") as open_file:
        return open_file.read()

@_user_config_function
def custom_task(
        input_files=(), files_that_must_exist=(), output_files=(), commands=()):
    clean(output_files)
    return runtime.create_task(
        input_files=input_files,
        files_that_must_exist=files_that_must_exist,
        output_files=output_files,
        commands=commands)

@_user_config_function
def serialize(**tasks):
    """
    Example usage:

    serialize(
        mkdir("foo"),
        copyFile("x.txt", "foo/x.txt"),
    )
    """
    prev_task = None
    for task in tasks:
        if prev_task is not None:
            task.input_tasks += (prev_task,)
        prev_task = task
    return task

# TODO: Finish this.  We need a way to remove the tasks that were created by
#       other tasks.  Maybe a task needs to keep track of which tasks its
#       post callback created.
@_user_config_function
def chain(task, *task_generators):
    """
    chain(
        first_task_returning_results(),
        lambda first_results: second_task_based_on_results(first_results),
        lambda second_results:_third_task_based_on_results(second_results),
    )

    For example:
    chain(
        read_file("version_file.txt"),
        lambda version: task_based_on_version(version),
    )
    """
    # TODO: The generated tasks need to remove themselves after all their
    #       child tasks have finished.
    # TODO: We can't simply cache made.runtime, since made.runtime is modified
    #       for each madefile that is loaded.  We need to cache
    #       made.runtime.__dict__.copy():
    # def save_runtime():
    #     runtime = imp.new_module("made.runtime")
    #     runtime.__dict__.update(globals())
    #     return runtime
    old_post_callback = task.post_callback

    def new_post_callback(finished_task):
        if old_post_callback is not None:
            old_post_callback(finished_task)

        next_task = task_generators[0](finished_task.return_value)

        if len(task_generators) > 1:
            chain(next_task, *task_generators[1:])

        next_task.input_tasks += (finished_task,)

    task.post_callback = new_post_callback

@_user_config_function
def clean(files_or_directories, description=None):
    if isinstance(files_or_directories, str):
        raise TypeError(
            "files_or_directories must be a sequence of strings, not a string")

    return runtime.create_remove_file_task(
        group_name="clean",
        files_to_delete=tuple(files_or_directories),
        description=description)

@_user_config_function
def mkdir(dir, required_parent_dir=""):
    # TODO: Remove the required_parent_dir parameter.
    # TODO: Add a group="default" parameter.
    required_parent_dirs = (
        (required_parent_dir,) if required_parent_dir != "" else ())

    return runtime.create_task(
        input_files=required_parent_dirs,
        output_files=(dir,),
        commands=[command.FunctionCall(
            "mkdir -p " + dir, _mkdir_and_missing_parents, dir)])

def _mkdir_and_missing_parents(dir):
    import os

    if not os.path.exists(dir):
        os.makedirs(dir)

@_user_config_function
def copy_file(source_file, dest_file):
    clean((dest_file,))
    return runtime.create_task(
        input_files=(source_file,),
        output_files=(dest_file,),
        commands=[command.FunctionCall(
            command.form_command_line(("cp", "-p", source_file, dest_file)),
            "__import__('shutil').copy2", source_file, dest_file)])

@_user_config_function
def copy_files(source_dir, dest_dir, file_names):
    # TODO: Check that there are no duplicates in file_names, to help
    #       the caller detect bugs.
    # Note that source_dir may be the empty string to mean the current
    # directory.
    input_files, output_files = [], []
    for file_name in file_names:
        source_file_path = misc.join_with_slashes(source_dir, file_name)
        dest_file_path = misc.join_with_slashes(dest_dir, file_name)

        input_files.append(source_file_path)
        output_files.append(dest_file_path)

    # Note that we cannot preserve the timestamps (e.g. by running cp -p).
    # Otherwise, since the input files will have different timestamps, some
    # output files will have earlier timestamps than some input files and
    # made will think the task is out of date.
    # TODO: Perhaps this is one case where we could use checksums to determine
    #       if the task is out of date?
    cp_command_sequence = ["cp"]
    if len(file_names) == 1:
        cp_command_sequence.append("-p")

    shell_command = ""
    if source_dir == "":
        shell_command = command.form_command_line(
            itertools.chain(cp_command_sequence, file_names, (dest_dir,)))
    else:
        for file_name in file_names:
            if shell_command != "":
                shell_command += " && \\\n"
            shell_command += command.form_command_line(
                itertools.chain(
                    cp_command_sequence, (source_file_path, dest_file_path)))

    clean(output_files)

    return runtime.create_task(
        input_files=input_files,
        output_files=output_files,
        commands=[command.FunctionCall(
            shell_command, _copy_files, source_dir, dest_dir, file_names)])

def _copy_files(source_dir, dest_dir, file_names):
    import shutil
    import os
    import stat
    from made.util import misc

    if len(file_names) == 0:
        return

    if len(file_names) > 1:
        # Note that we cannot preserve the timestamps.  Otherwise, since the
        # input files will have different timestamps, some output files will
        # have earlier timestamps than some input files and made will think the
        # task is out of date.
        #
        # So, we instead set the output timestamp to the maximum of the input
        # timestamps.  This has the unfortunate side effect that if any of the
        # input files change, all the output files will be changed.  If finer
        # granularity is desired, individual tasks should be created for each
        # file.
        stat_values = [
            os.stat(misc.join_with_slashes(source_dir, file_name))
            for file_name in file_names]

        max_modification_time = max(
            stat_value[stat.ST_MTIME] for stat_value in stat_values)
        max_access_time = max(
            stat_value[stat.ST_ATIME] for stat_value in stat_values)

    for file_name in file_names:
        source_file_path = misc.join_with_slashes(source_dir, file_name)
        dest_file_path = misc.join_with_slashes(dest_dir, file_name)
        shutil.copy2(source_file_path, dest_file_path)

        if len(file_names) > 1:
            os.utime(dest_file_path, (max_access_time, max_modification_time))

def create_empty_file(file_name, input_files=()):
    clean((file_name,))
    return runtime.create_task(
        input_files=input_files,
        output_files=(file_name,),
        commands=[command.FunctionCall(
            command.form_command_line(("touch", file_name)),
            _create_empty_file, file_name)])

def _create_empty_file(file_name):
    open(file_name, "wb").close()

def _compile(
        source_files, library_name=None, binary_name=None, is_for_dynamic=None):
    # TODO: Somehow support debug builds.

    assert(library_name is not None or binary_name is not None)
    output_subdir = (
        _library_output_subdir(is_for_dynamic) if library_name is not None
        else None)
    object_files = [
        compilersettings.source_to_object_file(
            source_file,
            output_subdir=output_subdir,
            library_name=library_name,
            binary_name=binary_name)
        for source_file in source_files]

    tasks = []
    files_that_must_exist = []
    object_file_dir = os.path.dirname(object_files[0])
    if len(object_file_dir) != 0:
        tasks.append(mkdir(object_file_dir))
        files_that_must_exist.append(object_file_dir)

    all_output_files = []
    for source_file, object_file in zip(source_files, object_files):
        d_file = compilersettings.cpp_compiler_chain.d_files_from_object_files(
             [object_file])[0]

        output_files = [object_file, d_file]
        all_output_files.extend(output_files)
        # TODO: On Windows, we need to add a task that merges all the d files
        #       into a single makedepend file.

        commands = compilersettings.commands_to_compile(
            [source_file], is_debug=False, library_name=library_name,
            binary_name=binary_name, is_for_dynamic=is_for_dynamic,
            output_subdir=output_subdir)

        def _post_callback(d_file):
            return lambda task: _set_header_dependencies_from_d_file(
                task, d_file)

        tasks.append(runtime.create_task(
            input_files=(source_file,),
            files_that_must_exist=files_that_must_exist,
            output_files=output_files,
            commands=commands,
            description="compile %s" % source_file,
            post_callback=_post_callback(d_file)))

    # Add one clean task for all output files to make cleaning run faster.
    clean(all_output_files)

    return tasks

def _set_header_dependencies_from_d_file(task, d_file):
    # We cache the timestamp of the .d file in the task and only reload it
    # if it changed.
    # TODO: On Windows we don't currently output .d files so we'd need to
    #       parse the Makedepend file.
    d_file_mod_time = task.user_data.file_cache().mod_time(
        task.madefile.full_path_to_file(d_file))
    if getattr(task.user_data, "d_file_mod_time", None) != d_file_mod_time:
        task.user_data.d_file_mod_time = d_file_mod_time
        try:
            new_input_files = _parse_source_dependencies(d_file)
        except IOError:
            # We couldn't open the .d file.  This can happen, for example,
            # when the compile task was interrupted before it could write out
            # the .d file.
            # TODO: Somehow return an error so the client can display it.
            return

        task.set_input_files(new_input_files)

def _parse_source_dependencies(d_file):
    # TODO: We need to support the equivalent of gcc's -MP option, where it
    #       creates phony targets for the header files.  Otherwise, if you
    #       delete/rename a header file any tasks to compile source files
    #       that needed it will fail because the input header file will no
    #       longer exist.  Perhaps we need a way to specify input files that
    #       aren't required to exist?

    # The .d file will be in the following format:
    #
    # source_file.o: header1.h header2.h \
    #    header3.h
    #
    # header1.h:
    #
    # header2.h:
    #
    # header3.h:

    deps = []
    with open(d_file, "r") as dep_file:
        is_first_line = True
        for line in dep_file:
            if is_first_line:
                # Note header files might have drive letters, so be careful to
                # only split at the first colon.
                is_first_line = False
                line = line.split(":", 1)[1]

            file_names = line.strip().split(" ")
            if len(file_names) == 0:
                break

            if file_names[-1] != "\\":
                deps.extend(file_names)
                break

            deps.extend(file_names[:-1])

    return deps

@_user_config_function
def library(name, source_files, dynamic=True, static=True):
    types_to_link = []
    if dynamic:
        types_to_link.append(True)
    if static:
        types_to_link.append(False)

    # Add tasks to compile the files.
    tasks = []
    if compilersettings.cpp_compiler_chain.name == "msvc":
        # With msvc, we need to compile the file differently depending on
        # whether it's going in a dynamic or static library.
        for is_for_dynamic in types_to_link:
            tasks.extend(_compile(
                source_files, library_name=name,
                is_for_dynamic=is_for_dynamic))

        _dummy_object_files(source_files, name, types_to_link)
    else:
        tasks.extend(_compile(source_files, library_name=name))

    # Add a task to perform the linking.
    for is_for_dynamic in types_to_link:
        # If we're linking a static library then we don't need to list any
        # input libraries.
        if is_for_dynamic:
            input_libs = runtime.user_config.input_libs_for_libs(
                (name,), is_for_dynamic)
            assert(os.path.splitext(input_libs[0].name)[0] == "lib" + name)
            input_libs = input_libs[1:]
        else:
            input_libs = ()

        tasks.append(_create_link_task(
            name, source_files, input_libs, is_for_dynamic, library_name=name))

    # TODO: Run makedepend on Windows.
    # TODO: CL can output included files, so we don't need to run makedepend
    #       (unless we want compatibility with the existing make output).

    return tasks

def _dummy_object_files(source_files, library_name, types_to_link):
    for source_file in source_files:
        # The makefiles need an empty object file in the current directory
        # (not in the "ds" or "us" subdirectories) so create a task to
        # generate one after the real object file(s) have been compiled.
        dummy_object_file = compilersettings.source_to_object_file(
            source_file, library_name=library_name)

        object_files = [
            compilersettings.source_to_object_file(
                source_file,
                output_subdir=_library_output_subdir(is_for_dynamic),
                library_name=library_name)
            for is_for_dynamic in types_to_link]

        create_empty_file(dummy_object_file, input_files=object_files)

def _library_output_subdir(is_for_dynamic):
    if platform.system() != "Windows":
        return None
    return ("ds" if is_for_dynamic else "us")

@_user_config_function
def binary(
        name, source_files, toplevel_library_dependencies,
        use_dynamic_libraries, subsystem=None):
    tasks = _compile(
        source_files, binary_name=name, is_for_dynamic=use_dynamic_libraries)

    input_libs = runtime.user_config.input_libs_for_libs(
        toplevel_library_dependencies, use_dynamic_libraries)
    tasks.append(_create_link_task(
        name, source_files, input_libs, use_dynamic_libraries,
        binary_name=name, subsystem=subsystem))

    # TODO: Run makedepend on Windows.

    return tasks

# TODO: The name is either the library or binary name, so we just need an
#       is_library parameter instead of library_name and binary_name parameters.
def _create_link_task(
        name, source_files, input_libs, use_dynamic_libraries,
        library_name=None, binary_name=None, subsystem=None):
    assert(library_name is None or binary_name is None)
    is_library = library_name is not None

    object_files = list(
        compilersettings.source_to_object_files(
            source_files,
            output_subdir=(_library_output_subdir(use_dynamic_libraries)
                if is_library else None),
            library_name=library_name,
            binary_name=binary_name))

    # If we're intending to build with dynamic libraries but we were given
    # a full path to a static library, include it in the object files.
    if use_dynamic_libraries:
        for input_lib in input_libs[:]:
            if not input_lib.is_dynamic:
                assert(input_lib.full_path_to_static_library_or_none
                    is not None)
                object_files.append(
                    input_lib.full_path_to_static_library_or_none)
                input_libs.remove(input_lib)

    input_lib_names = [
        input_lib.full_path_to_static_library_or_none or input_lib.name
        for input_lib in input_libs]
    # TODO: Support debug builds.
    # TODO: Mark the import libs as being output and make the tasks that use
    #       them list them as inputs.
    # This module accepts library names without the "lib" prefix, but
    # compilersettings and compilercommands do not assume there is a "lib"
    # prefix.
    name_with_any_lib_prefix = (
        "lib" + name if library_name is not None else name)
    commands = compilersettings.commands_to_link(
        name_with_any_lib_prefix, object_files, input_lib_names,
        use_dynamic_libraries, is_library,
        is_debug=False, subsystem=subsystem)

    dynamic_built_input_lib_paths = [
        _input_lib_path(input_lib)
        for input_lib in input_libs
        if input_lib.is_built and input_lib.is_dynamic]
    static_built_input_lib_paths = tuple(
        _input_lib_path(input_lib)
        for input_lib in input_libs
        if input_lib.is_built and not input_lib.is_dynamic)

    output_file = compilersettings.link_output_file_path(
        name_with_any_lib_prefix, use_dynamic_libraries, is_library)
    clean(
        (output_file,),
        description="remove %s" % os.path.basename(output_file))

    return runtime.create_task(
        input_files=(os.path.dirname(output_file),) +
            tuple(object_files) + static_built_input_lib_paths,
        files_that_must_exist=dynamic_built_input_lib_paths,
        output_files=(output_file,),
        commands=commands,
        description="link %s" % os.path.basename(output_file))

def source_to_object_files_for_library(
        source_file, library_name, use_dynamic_libraries):
    return [source_to_object_file_for_library(
            source_file, library_name, use_dynamic_libraries)
        for source_file in source_files]

def source_to_object_file_for_library(
        source_file, library_name, use_dynamic_libraries):
    return compilersettings.source_to_object_file(
        source_file,
        output_subdir=_library_output_subdir(use_dynamic_libraries),
        library_name=library_name)

def _input_lib_path(input_lib):
    return compilersettings.link_input_file_path(
        input_lib.name, input_lib.is_dynamic)

def install_user_config(unexpanded_lib_deps={}):
    for name, entry in _user_config_entries.items():
        runtime.add_user_config(entry, name)

    from made.contrib import libdeps
    libdeps.set_unexpanded_lib_deps(unexpanded_lib_deps)
    runtime.add_user_config(libdeps.input_libs_for_libs)

